feat(payments): switch-provider recovery flow + inline guided wizard (#43 US3+US4)#64
Open
TortoiseWolfe wants to merge 2 commits intomainfrom
Open
feat(payments): switch-provider recovery flow + inline guided wizard (#43 US3+US4)#64TortoiseWolfe wants to merge 2 commits intomainfrom
TortoiseWolfe wants to merge 2 commits intomainfrom
Conversation
US3+US4) Foundation for the SwitchProviderPanel UI in the next commit. Doesn't change any user-visible behavior on its own — adds the data plumbing that lets the UI mount a fresh PaymentButton from a previously-failed parent intent and preserve the audit chain across providers. Service surface: - createPaymentIntent gains optional parent_intent_id arg. When set, the INSERT carries it through to the payment_intents row so the parent_intent_id column shipped in PR #63 actually gets populated for cross-provider retries (not just same-provider retries via retryFailedPayment). - New getParentIntentForRetry(intentId) returns { amount, currency, type, interval, customer_email, description, retry_count } — the fields needed to seed PaymentButton. Mirrors retryFailedPayment's server-side guards (PaymentRetryLimitError + PaymentRetryExpiredError) so the recovery panel fails fast before mounting. Cooling is intentionally NOT enforced here — switching providers does not reuse the parent's idempotency_key, so cooling protects against nothing in this path. - usePaymentButton accepts a parentIntentId option; plumbed through to createPaymentIntent. Audit shape extensions (non-breaking — event_data is JSONB): - recovery_method: 'same_provider' | 'switch_provider' (defaults to same_provider so existing call-sites stay unchanged) - selected_provider: PaymentProvider (set when switching) Admin dashboard's audit reader already shows raw event_data so the new fields surface for free. Spec amendment (features/payments/040-payment-retry-ui/spec.md): - Status flipped to "Mostly Shipped (recovery UX shipped; saved-method storage out of scope)" - US3 reframed: "switch payment method (provider switch)" replaces literal "Update Payment Method (saved cards)". ScriptHammer never stores cards — every checkout is a fresh provider session — so the honest interpretation in this codebase is "after a card decline, let the user pick a different provider". Saved-card storage (Stripe Customer + saved_payment_methods + stripe.js Elements) is a separate multi-PR feature behind a PCI scope review. - US4 reframed: inline progressive disclosure replaces a dedicated wizard component. The recovery list escalates with retry_count. - Both reframings preserve the user-facing intent of FR-011-FR-019. Tests: 4 new in retry.test.ts (getParentIntentForRetry happy path, limit guard, expired guard, no-cooling-guard semantics). All 24 retry tests green, full suite 3269/3269 across 288 files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-result (#43 US3+US4) Closes the remaining 2 of 7 gaps in #43, reframed for ScriptHammer's static-export architecture (saved-card storage is out of scope; honest interpretation is "switch payment method (provider)" + "guided escalation via progressive disclosure"). US3 — SwitchProviderPanel (5-file component): - Reads parent intent via getParentIntentForRetry, seeds PaymentButton pre-filled with parent's amount + currency + type + email + description. - Reuses PaymentButton's multi-provider machinery (Stripe / PayPal / Cash App / Chime), GDPR consent gating per provider, and offline queue. Nothing rebuilt. - New intent links to parent via parent_intent_id so the audit chain spans providers (matches schema added in PR #63). - Handles limit-reached + expired + generic-error states; shows masked "switching from previous attempt: $X" callout with the parent's formatted amount + description. - 8 unit tests + 1 a11y test. US4 — Inline progressive recovery disclosure on PaymentStatusDisplay: - retry_count = 0: just the retry button (existing behavior). - retry_count = 1: collapsed <details>: "Need more help?" hint. - retry_count >= 2: <details open> showing the recovery list: 1. Try again — payment failures are sometimes temporary. 2. Use a different payment method (Stripe / PayPal / Cash App / Chime). 3. Contact support with the transaction reference above. - retry_count >= 3 (cap): step 1 is struck through; step 3 is bold — emphasizes support contact (FR-019). - Honors FR-016 (guided steps), FR-017 (prioritized: retry → switch → support), FR-018 (alternative payment options), FR-019 (escalation). - No new component file for the wizard — structure inside the existing failed-state block, since the categorization + counter + cooling shipped in PR #63 is the focal point. - 7 new unit tests for switch button visibility, panel toggle, recovery disclosure escalation across retry_count values. UI restructure of failed-state block (PaymentStatusDisplay): - Counter + retry button + "Use a different payment method" button on one row (flex-wrap on mobile). - Switch button has aria-expanded + aria-controls bound to the panel. - Inline panel mount below the action row. - Recovery disclosure below that. E2E (tests/e2e/payment/03-failed-payment-retry.spec.ts): - Two new test.skip stubs documenting the Stripe-Checkout gate. Both scenarios are exercised by unit tests; the e2e versions need a real failed payment_results row that requires Stripe API keys to produce. Same gate as the other Stripe-Checkout skips in this file. Verification: - pnpm test: 3269/3269 across 288 files (20 new for this PR) - pnpm test:rls: 55/55 (no schema changes; sanity) - pnpm run type-check, lint: clean #43 still open after this PR — gaps 1-7 closed in spirit, but the literal "saved cards" interpretation of FR-011-FR-015 stays out of scope and the issue tracks any future architectural change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the remaining 2 of 7 gaps in #43 (US3 + US4) — reframed for ScriptHammer's static-export architecture.
PR #62 corrected the false 'route missing' framing of the original issue. PR #63 closed gaps 1-5 (idempotency-key reuse, attempt counter + cooling, error categorization, offline banner, audit log). This PR closes gaps 6-7 with two architecture-fit reframings recorded in the spec amendment commit.
US3 reframed — "Switch Payment Method" (FR-011-FR-015)
The spec's literal 'Update Payment Method' assumes saved cards + stripe.js Elements + a PCI surface. ScriptHammer never stores cards — every checkout is a fresh Stripe Checkout (or PayPal redirect, or Cash App / Chime direct link). The honest interpretation in this codebase is: after a card decline, let the user pick a different provider.
New `` 5-file component:
US4 reframed — Inline progressive disclosure (FR-016-FR-019)
Replaces a dedicated wizard component with structure inside the failed-state block of `` that escalates with `retry_count`:
Details
`: 'Need more help?'Honors FR-016 (guided steps), FR-017 (prioritized: retry → switch → support), FR-018 (alternative payment options), FR-019 (escalation).
Commits
Out of scope
Test plan
E2E — #53 progress
Two new `test.skip` stubs added with explicit comments pointing to the unit tests that exercise the same logic. They follow the same Stripe-API-keys gate as the other Checkout-driven skips. The unit-level coverage is the load-bearing test surface for these flows.
Sharp edges
🤖 Generated with Claude Code